/*
 * Copyright (C) 2014 luke.
 * 
 * This file is part of Silvie.
 * 
 * Silvie is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Silvie is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Silvie.  If not, see <http://www.gnu.org/licenses/>.
 */

package silvie;

import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutionException;
import java.util.prefs.Preferences;

import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;

import silvie.gui.FileExplorer;
import silvie.gui.Preview;
import silvie.gui.SilvieGUI;
import silvie.gui.filetree.FileTreeModel;
import silvie.gui.previewers.AbstractPreviewer;
import silvie.resources.AbstractResource;
import silvie.resources.ResourceFormat;
import utils.DirectoryWatcher;

/**
 * Controller class. It contains the "callback" methods and manages the
 * application's preferences and GUI.
 * 
 * @author luke
 */
public class Silvie {
	private static final String DISCLAIMER = "This is free software, see the COPYING file for more details.";
	private static final String FIXED_PALETTE_PATH = "silvie/fixed.pal";
	private static final String OPTIONS_PALETTE_PATH = "silvie/options.pal";
	public static final int PALETTE_SIZE = 768;
	public static final String DATA_PATH_PREFERENCES_KEY = "data_path";
	private static final int OUTDATED_PATH_CHECK_INTERVAL = 200;
	private static final String WATCH_SERVICE_THREAD_NAME = "WatchService Thread";
	private static final int OPEN_KEY = KeyEvent.VK_O;
	private static final int SAVE_KEY = KeyEvent.VK_S;
	private static final int DEFAULT_TEXT_KEY = KeyEvent.VK_ESCAPE;
	public static final byte[] FIXED_PALETTE = new byte[PALETTE_SIZE];
	public static final byte[] OPTIONS_PALETTE = new byte[PALETTE_SIZE];
	private Preferences preferences;
	private KeyboardFocusManager keyboardFocusManager;
	private KeyEventDispatcher keyEventDispatcher;
	private SilvieGUI silvieGUI;
	private FileExplorer fileExplorer;
	private Preview preview;
	private DirectoryWatcher currentWatcher;
	private AbstractPreviewer currentPreviewer;

	/**
	 * Convenience interface to be used in a lambda with the
	 * {@link Silvie#doKeyboardAction(KeyboardAction)} method.
	 * 
	 * @author luke
	 */
	@FunctionalInterface
	private interface KeyboardAction {
		/**
		 * This method is called when the associated keyboard shortcut is
		 * pressed.
		 */
		void doAction();
	}

	/**
	 * Main method. It reads both palettes from the classpath and creates the
	 * Silvie instance.
	 * 
	 * <p>
	 * A Silver palette is a 768-byte file indexing 3-byte RGB colors. Silver
	 * contains two such files: fixed.pal and options.pal. These two files are
	 * included in the {@code silvie} package for convenience.
	 * 
	 * @param args unused.
	 * @throws IOException if a file error occurs.
	 */
	public static void main(String args[]) throws IOException {
		System.out.println(DISCLAIMER);
		ClassLoader classLoader = Silvie.class.getClassLoader();
		classLoader.getResourceAsStream(FIXED_PALETTE_PATH).read(FIXED_PALETTE);
		classLoader.getResourceAsStream(OPTIONS_PALETTE_PATH).read(OPTIONS_PALETTE);
		new Silvie();
	}

	/**
	 * Silvie constructor. It retrieves the application's preferences node,
	 * launches the GUI creation and sets the Silver data path and keyboard
	 * shortcuts.
	 */
	public Silvie() {
		this.preferences = Preferences.userNodeForPackage(Silvie.class);
		SwingUtilities.invokeLater(() -> {
			this.silvieGUI = new SilvieGUI(this);
			this.fileExplorer = this.silvieGUI.getFileExplorer();
			this.preview = this.silvieGUI.getPreview();
			if (getDataPath() == null) {
				this.silvieGUI.openDataDirectory(this.preferences, null);
			}
			if (getDataPath() != null) {
				applyDataPath();
			}
			this.silvieGUI.setFramesVisible(true);
		});
		// Periodically check that the Silver data directory exists
		new Timer(OUTDATED_PATH_CHECK_INTERVAL, e -> {
			if (getDataPath() == null && this.fileExplorer.getTreeModel() != null) {
				unregisterCurrentWatcher();
				this.fileExplorer.setFileTreeModel(null);
			}
		}).start();
		setKeyboardShortcuts();
		this.currentWatcher = null;
		this.currentPreviewer = null;
	}

	/**
	 * Retrieves the Silver data directory from the preferences.
	 * 
	 * @return the data directory's path or null if it hasn't been set (or is
	 *         set but doesn't exist anymore).
	 */
	private String getDataPath() {
		String dataPath = this.preferences.get(DATA_PATH_PREFERENCES_KEY, null);
		if (dataPath != null && Paths.get(dataPath).toFile().exists()) {
			return dataPath;
		}
		return null;
	}

	/**
	 * Creates a FileTreeModel instance with the Silver data directory as its
	 * root and sets the tree's model to it. This method also manages the
	 * WatchService thread so any modification of the data directory (deleted
	 * files, renamed files etc) are automatically reflected in the tree.
	 */
	private void applyDataPath() {
		unregisterCurrentWatcher();
		String dataPath = getDataPath();
		FileTreeModel model = new FileTreeModel(new File(dataPath));
		try {
			// Watch for changes in the data directory and notify the model
			this.currentWatcher = new DirectoryWatcher(Paths.get(dataPath), true, (kind, path) -> {
				SwingUtilities.invokeLater(() -> model.fireTreeStructureChanged(path));
				return null;
			});
			new Thread(() -> this.currentWatcher.processEvents(), WATCH_SERVICE_THREAD_NAME).start();
		} catch (IOException e) {
			e.printStackTrace();
			Silvie.this.preview.setCanvas(e);
		}
		this.fileExplorer.setFileTreeModel(model);
	}

	/**
	 * Sets the "Save resource", "Open data directory" and ESC keyboard
	 * shortcuts.
	 */
	private void setKeyboardShortcuts() {
		this.keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
		this.keyboardFocusManager.addKeyEventDispatcher(this.keyEventDispatcher = e -> {
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				int keyCode = e.getKeyCode();
				if (e.isControlDown()) {
					switch (keyCode) {
					case SAVE_KEY:
						doKeyboardAction(() -> save());
						return true;
					case OPEN_KEY:
						doKeyboardAction(() -> {
							boolean approved = Silvie.this.silvieGUI.openDataDirectory(Silvie.this.preferences, getDataPath());
							if (approved && getDataPath() != null) {
								applyDataPath();
							}
						});
						return true;
					}
				} else if (keyCode == DEFAULT_TEXT_KEY) {
					closeCurrentPreviewer();
					Silvie.this.preview.displayDefaultText();
					return true;
				}
			}
			return false;
		});
	}

	/**
	 * Provides a convenient way to execute actions triggered by keyboard
	 * shortcuts and ensure non-overlapping execution. For example, pressing
	 * Ctrl+S twice should show the save dialog only once.
	 * 
	 * @param action the KeyboardAction implementation whose
	 *        {@link KeyboardAction#doAction()} method will be called.
	 */
	private void doKeyboardAction(KeyboardAction action) {
		this.keyboardFocusManager.removeKeyEventDispatcher(this.keyEventDispatcher);
		action.doAction();
		this.keyboardFocusManager.addKeyEventDispatcher(this.keyEventDispatcher);
	}

	/**
	 * Called by the GUI when a file is selected in the tree. This method
	 * launches the preview of the selected file if it is supported.
	 * 
	 * @param filepath the path to the file.
	 * @param format a ResourceFormat for the file (null if unsupported).
	 */
	public void preview(Path filepath, ResourceFormat format) {
		File file = filepath.toFile();
		if (file.isFile() && format != null) {
			closeCurrentPreviewer();
			new SwingWorker<AbstractPreviewer, Void>() {
				@Override
				protected AbstractPreviewer doInBackground() throws Exception {
					AbstractResource resource = format.getFactoryFunction().apply(file);
					resource.extract();
					return resource.getPreviewer(Silvie.this.preview);
				}

				@Override
				protected void done() {
					try {
						AbstractPreviewer previewer = get();
						previewer.doPreview();
						Silvie.this.currentPreviewer = previewer;
					} catch (InterruptedException e) {
						e.printStackTrace();
						Silvie.this.preview.setCanvas(e);
					} catch (ExecutionException e) {
						e.printStackTrace();
						Silvie.this.preview.setCanvas(e.getCause());
					}
				}
			}.execute();
		}
	}

	/**
	 * Called when pressing Ctrl+S while previewing a resource file. This method
	 * launches the saving of the selected resource.
	 */
	public void save() {
		if (this.currentPreviewer != null && this.currentPreviewer.save()) {
			new SwingWorker<File, Void>() {
				@Override
				protected File doInBackground() throws Exception {
					return Silvie.this.currentPreviewer.doSave();
				}

				@Override
				protected void done() {
					try {
						get();
					} catch (InterruptedException e) {
						e.printStackTrace();
						Silvie.this.preview.setCanvas(e);
					} catch (ExecutionException e) {
						e.printStackTrace();
						Silvie.this.preview.setCanvas(e.getCause());
					}
				}
			}.execute();
		}
	}

	/**
	 * Closes the resources held by the current previewer.
	 */
	public void closeCurrentPreviewer() {
		if (Silvie.this.currentPreviewer != null) {
			Silvie.this.currentPreviewer.close();
		}
	}

	/**
	 * Closes the current WatchService.
	 */
	public void unregisterCurrentWatcher() {
		if (this.currentWatcher != null) {
			this.currentWatcher.unregister();
		}
	}
}
